Android群英传 3 Android控件架构与自定义控件详解

Android控件架构

控件大致被分为两类,即ViewGroup控件与View控件。
通过ViewGroup,整个界面的控件形成了一个树形结构控件树。上层控件负责下层子控件的测量与绘制,并传递交互事件。findViewById()方法,就是在控件树以数深度优先来遍历查找对应的元素。
每颗控件树的顶部,都有一个ViewParent对象,为整棵树的控制核心,所有的交互管理事件都由它统一调度和分配,从而可以对整个视图进行整体控制。

屏幕快照 2018-07-17 下午2.31.03
屏幕快照 2018-07-17 下午2.31.03

通常,在Activity中使用setContentView()方法来设置一个布局,在调用该方法后,布局内容才真正的显示出来。
屏幕快照 2018-07-17 下午2.36.21
屏幕快照 2018-07-17 下午2.36.21

每个Activity都包含一个Window对象,由PhoneWindow来实现。PhoneWindow将一个DecorView设置为整个应用窗口的根View。DecorView作为窗口界面的顶层视图,封装了一些窗口操作的通用方法。DecorView将要显示的具体方法呈现在PhoneWindow上,这里面的所有View的监听事件通过WindowManageService来进行验收,并通过Activity对象来回调相应的onClickListener。
在显示上,它将屏幕分为两部分,TitleView和ContentView。
屏幕快照 2018-07-17 下午2.45.14
屏幕快照 2018-07-17 下午2.45.14

其中ViewGroup会根据对应参数设置不同的布局,如最常用的布局。而如果用户通过设置requestWindowFeature(Window.FEATURE_NO_TITLE)来设置全屏显示,视图树中的布局就只有Content了,这就解释了为什么调用requestWindowFeature()方法一定要在调用setContentView()方法之前才能生效的原因。
在代码中,当程序在onCreate()方法中调用setContentView()方法后,ActivityManagerService会回调onResume()方法,此时系统才会把整个DecorView添加到PhoneWindow中,并让其显示出来,从而最终完成界面的绘制。

View的测量

系统在绘制View前,必须对View进行测量,即告诉系统该画一个多大的View,在onMeasure()方法中进行。
通过系统提供的设计精悍功能强大的类MeasureSpec类来测量View。MeasureSpec为32位int值,高2位位测量的模式,低30位为测量的大小,在计算中使用位运算是为了提高并优化效率。
测量模式为3种:

  1. EXACTLY
    即精确值模式,当我们将控件的layout_width或layout_height属性指定为具体数值时,或指定为match_parent属性时(占据父View的大小),系统使用的是EXACTLY模式。
  2. AT_MOST
    即最大值模式,当控件的layout_width或layout_height属性指定为wrap_content时,控件大小一般岁总监的子控件或内容的变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。
  3. UNSPECIFID
    不指定大小测量模式,View想多大就多大,通常情况下在绘制自定义View时才会使用。

View类默认的onMeasure()方法只支持EXACTLY模式,所以如果自定义控件时不重写onMeasure方法的话,就只能使用EXACTLY模式。控件可以响应你指定的具体宽高值或是match_parent属性。而如果要让自定义View支持wrap_content属性,那么就必须重写onMeasure()方法来指定wrap_content时的大小,如果不重写,就不知道该使用默认多大的尺寸,因此,就会默认填充整个父布局,所以重写onMeasure()方法的目的,就是为了能够给View一个wrap_content属性下的默认大小。

View的绘制

测量好一个View后,我们就可以重写onDraw()方法,在Canvas对象上来绘制所需要的图形。
当创建一个Canvas对象时,需要传进去一个bitmap对象。这个bitmap用来存储所有绘制在Canvas上的像素信息。调用所有的Canvas.drawXXX方法都发生在这个bitmap上。

##ViewGroup的测量
ViewGroup会去管理子View,就有负责子View的显示大小。当ViewGroup大小为wrap_content时,就需要对子View进行遍历,以获得所有子View的大小,从而决定自己的大小。其他模式则会通过具体的指定值来设置自身的大小。
ViewGroup在测量时通过遍历所有的子View,从而调用View的Measure方法来获得每一个子View的测量结果。
当子View测量完毕时,就需要将子View放到合适的位置,这个过程就是View的Layout过程。ViewGroup在执行Layout过程时,同样是使用遍历来调用子View的Layout方法,并制定其具体显示的位置从而来决定其布局位置。
在定义ViewGroup时,通常会重写onLayout方法来控制其子View显示位置的逻辑。同样,如果要支持wrap_content属性,必须重写onMeasure。

ViewGroup的绘制

ViewGroup通常不需要绘制,如果不是指定了ViewGroup的背景颜色,ViewGroup的onDraw方法都不会被调用,但是,ViewGroup会使用dispatchDraw()方法来绘制其子View,其过程同样是通过遍历所有的子View,并调用子View绘制方法来完成绘制。

自定义View

在自定义View时,我们通常会去重写onDraw()来绘制View的显示内容,如果该View还需要使用wrap_content属性,那么还必须重写onMeasure()方法。另外,通过自定义attrs属性,还可以设置新的属性配置值。
在View中通常有以下一些比较重要的回调方法。

  • onFinishInflate():从XML加载组件后回调。
  • onSizeChanged():组件大小改变时回调。
  • onMeasure():回调该方法来进行测量。
  • onLayout():回调该方法来确定显示的位置。
  • onTouchEvent():监听到触摸事件时回调。

通常有以下方法来实现自定义的控件。

  • 对现有控件进行扩展。
  • 通过组合来实现新的控件。
  • 重写View来实现全新的控件。

对现有控件进行扩展

可以再onDraw()方法中对原生控件行为进行扩展。
程序调用super.onDraw(canvas)方法来实现原生控件的功能。

创建复合控件

创建复合控件可以很好地创建出具有重用功能的控件集合。这种方式通常需要继承一个合适的ViewGroup,再给它添加指定功能的控件,再给它添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们一般会给它指定一些可配置的属性,让它具有更强的扩展性。

定义属性

为一个View提供可自定义的属性非常简单,只需要在res资源目录的values目录下创建一个attrs.xml的属性定义文件,并在该文件中通过代码定义相应的属性即可。

屏幕快照 2018-07-17 下午5.04.48
屏幕快照 2018-07-17 下午5.04.48

在代码中通过< declare-styleable>标签声明了使用自定义属性,通过name属性来确定引用的名称。通过< attr>标签来声明具体的自定义属性。通过format属性来指定属性的类型。
在确定好属性后,就可以创建一个自定义控件,让它继承自ViewGroup,从而组合一些需要的控件。在构造方法中,通过TypedArray对象的getString()和getColor()等方法,就可以获取这些定义的属性值。

屏幕快照 2018-07-17 下午5.11.26
屏幕快照 2018-07-17 下午5.11.26

屏幕快照 2018-07-17 下午5.12.42
屏幕快照 2018-07-17 下午5.12.42
屏幕快照 2018-07-17 下午5.13.03
屏幕快照 2018-07-17 下午5.13.03

当获取完所有的属性值后,需要调用TypedArray的recyle方法来完成资源的回收。

组合控件

通过动态添加控件的方式,使用addView()方法将控件加入到模板中,并给它们设置前面所获取到的具体的属性值,如文字颜色、大小等。

屏幕快照 2018-07-17 下午5.16.29
屏幕快照 2018-07-17 下午5.16.29

定义接口

屏幕快照 2018-07-17 下午5.21.03
屏幕快照 2018-07-17 下午5.21.03

暴露接口给调用者

屏幕快照 2018-07-17 下午5.25.21
屏幕快照 2018-07-17 下午5.25.21

屏幕快照 2018-07-17 下午5.25.46
屏幕快照 2018-07-17 下午5.25.46

实现接口回调
在调用者的代码中,调用者需要实现这样一个接口,并完成接口中的方法。

屏幕快照 2018-07-17 下午5.28.58
屏幕快照 2018-07-17 下午5.28.58

除了通过接口回调的方式来动态的控制UI模板,同样可以使用公共方法来动态地修改UI模板中的UI。这样可以进一步提高模板的可定制性。

屏幕快照 2018-07-17 下午5.31.54
屏幕快照 2018-07-17 下午5.31.54

引用UI模板

在需要使用的地方引用UI模板,在引用前,需要指定第三方控件的命名空间。

屏幕快照 2018-07-17 下午5.33.56
屏幕快照 2018-07-17 下午5.33.56

这行代码就是在指定引用的命名空间xmlns,即xml namespace。这里指定了名字空间为“android”,因此在接下来使用系统属性时,才可以使用“android:”来引用Android的系统属性。如果要使用自定义的属性,就需要创建自己的名字空间。在Android Studio中,第三方的控件都使用如下代码来引入名字空间。
屏幕快照 2018-07-17 下午5.39.45
屏幕快照 2018-07-17 下午5.39.45

这里我们将引入的第三方控件的名字空间取名为custom,之后再xml文件中使用自定义的属性时,就可以通过这个名字空间来引用。
屏幕快照 2018-07-17 下午5.41.56
屏幕快照 2018-07-17 下午5.41.56

使用自定义的View与系统原生的View的最大区别就是在申明控件时,需要指定完整的包名,而在引用自定义的属性时,需要使用自定义的xmls名字。
再更进一步,将这个UI模板写到一个布局文件中,在其他的布局文中,直接通过< include>标签来引用这个UI模板的View。

重写View来实现全新的控件

当Android系统原生的控件无法满足我们的需求时,我们就可以完全创建一个新的自定义View来实现需要的功能。通常需要继承View类,重写onDraw,onMeasure来实现绘制逻辑,同时重写onTouchEvent等触控时间来实现交互逻辑。还可以像实现组合控件那样,通过引入自定义属性,丰富自定义View的可定制性。

弧线展示图

音频条形图

自定义ViewGroup

重写onMeasure来对子View进行测量,重写onLayout确定子View的位置,重写onTouchEvent增加响应事件。

事件拦截机制分析

Android为触摸事件封装了一个类MotionEvent,里面封装了触摸点的坐标,点击事件的类型。

屏幕快照 2018-07-18 下午3.08.26
屏幕快照 2018-07-18 下午3.08.26

对于ViewGroup重写了三个方法:
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent
对于View来说,重写了两个方法:
onTouchEvent
dispatchTouchEvent
屏幕快照 2018-07-18 下午3.12.38
屏幕快照 2018-07-18 下午3.12.38

屏幕快照 2018-07-18 下午3.12.47
屏幕快照 2018-07-18 下午3.12.47

dispatchTouchEvent基本不用动。
事件拦截的核心方法是onInterceptTouchEvent返回值:True,拦截,不继续;False,不拦截,继续流程。
事件处理onTouchEvent返回值:True,处理了,不用给上级汇报了,上级不会继续处理了;False,给上级处理。
屏幕快照 2018-07-18 下午3.20.48
屏幕快照 2018-07-18 下午3.20.48